SpringSecurity 编写一个简单认证Demo
FIXME: 这篇笔记的 JWT 过滤器要修改一下,实际不能这么搞,因为引入 JWT 就是为了避免每次都查询数据库。下面这样还去查询数据库就太蠢了,以后可以使用 RSA 配置一个数字签名的认证
认证与鉴权
Spring Security 主要功能如下
- 认证 Authentication
- 授权 Authorization
- 攻击防护

认证的方式也可以有多种多样
Authentication 常见的有如下几种
- HTTP Authentication
- Forms Authentication
- Certificate
- Tokens
编写一个 Resource
其实就是随便写一个 API
@RestController
public class HelloResource {
@GetMapping("/hello")
public String hello() {
return "this is resource";
}
}
编写 UserDetailsService
/**
* 编写一个自定义的 UserDetailsService 用来加载用户
* 注意,它一般不做密码校验,单纯是给 Security 其它组件
* 提供数据,至于密码校验是由 AuthenticationManager 完成的
**/
@Service
public class MyDetailsService implements UserDetailsService {
/**
* 这个 UserDetailsService 一般只用于到 DAO 层加载用户数据
*/
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
// 这里使用官方提供的 User 类,第三个参数是权限列表,这里直接让它为空
return new User("foo", "foopassword", new ArrayList<>());
}
}
编写 SecurityConfigurer
/**
* 这里首先继承了 WebSecurityConfigurerAdapter,它是所有 Web配置的接入点
* Adapter 即适配器
* <p>
* 注意 @EnableWebSecurity 注解内置了 @Configurable
**/
@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private MyDetailsService myDetailsService;
/**
* 顾名思义,就是建造者模式,它用来构建一个 AuthenticationManager
* 添加 UserDetailsService 和 AuthenticationProvider's 就在这里
* <p>
* 然后它还可以用来
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
// 不做具体的 AuthenticationManager 选择这里的默认使用 DaoAuthenticationConfigurer
// 这个 DetailsService 单纯就是从 Dao 层取得用户数据,它不进行密码校验
.userDetailsService(myDetailsService)
// 如果上面那个 userDetailsService 够简单其实可以像下面这样用 SQL 语句查询比对
// .dataSource(dataSource)
// .usersByUsernameQuery("Select * from users where username=?")
// 这个 passwordEncoder 配置的实际就是 DaoAuthenticationConfigurer 的加密器
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
// 注意,虽然显示过时了,但是官方没有计划删除它,一般也就使用纯文本密码的测试时会用它
return NoOpPasswordEncoder.getInstance();
}
}
访问测试
输入项目地址访问
http://localhost:8080/hello
然后会自动跳转到登陆页面要求登陆(默认使用了 formLogin 这个过滤器)

整合 JWT

首先是导入依赖
<properties>
<jwt.version>0.10.7</jwt.version>
</properties>
<!-- ... -->
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
创建一个 JWT 工具类
@Service
public class JwtUtil {
// 注意,这里使用 secretKeyFor 方法自动随机生成一个适合指定编码长度的密钥,避免硬编码出错,以及安全问题
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 不过正式的开发环境,这个密钥最好不要这样搞,第一次生成之后记录下来就行了,不然每次重启服务一次,全部 JWT 都失效了
public String extractUsername(String token) {
// 这里直接引用 Claims 类里面的 getSubject 方法
return extractClaim(token, Claims::getSubject);
}
/*
它等价于下面这个
public String extractUsername(String token) {
return extractClaim(token, (Claims claims)-> {
return claims.getSubject();
});
}
*/
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// 这个 Function 表示一个接受一个参数并产生结果的函数。
// <T> 函数输入的类型(就是 apply 方法的参数类型)
// <R> 函数结果的类型(就是 apply 方法的返回值)
public <R> R extractClaim(String token, Function<Claims, R> claimsResolver) {
final Claims claims = extractAllClaims(token);
// 这个 Function 函数接口通过调用 apply 取得结果
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
// 这里需要显示指定使用 HS256(注意,上面只是生成一个适合长度的密钥,本体它还是一个普通字串)
.signWith(SECRET_KEY, SignatureAlgorithm.HS256).compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
// 检查 token 里面的信息是否与 UserDetails 相同,这里可以写多几个认证,但是只是测试,所以象征性比对个用户 名就行了
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}